#include "stdafx.h"
#include "camera.h"

#include "glc/clientstate.h"
#include "util/mat4.h"
//#include "util/vec4.h"
//#include "core/coresettings.h"
#include <math.h>
#include "iostream"

namespace Demo {

// amount of different zoom heights
static const double ANGLE_Z_MULTIPLIER = 3.0;
static const double ANGLE_Z_ROTATING_COEFF = 0.6;
static const int ZOOM_STATES_COUNT = 15;
static const int ZOOM_DEFAULT_STATE = 10;
static const double ZOOM_STEP = 1.1;
static const double ZOOM_STATES[ZOOM_STATES_COUNT] = {
    pow(ZOOM_STEP, -10),
    pow(ZOOM_STEP, -9),
    pow(ZOOM_STEP, -8),
    pow(ZOOM_STEP, -7),
    pow(ZOOM_STEP, -6),
    pow(ZOOM_STEP, -5),
    pow(ZOOM_STEP, -4),
    pow(ZOOM_STEP, -3),
    pow(ZOOM_STEP, -2),
    pow(ZOOM_STEP, -1),
    pow(ZOOM_STEP, +0),
    pow(ZOOM_STEP, +1),
    pow(ZOOM_STEP, +2),
    pow(ZOOM_STEP, +3),
    pow(ZOOM_STEP, +4)
};

Camera::Camera()
    :m_zoomStateIndex(ZOOM_DEFAULT_STATE)
    ,m_x(0)
    ,m_y(10)
    ,m_z(10)
    ,angle_x(3.14)
    ,angle_z(-0.314)
    ,MOUSE_SENSIVITY(0.01)
    ,CAMERA_SPEED(0.1)
{
}

void Camera::shiftRight()
{
    m_x += cos(angle_x) * CAMERA_SPEED;
    m_y -= sin(angle_x) * CAMERA_SPEED;
}

void Camera::shiftLeft()
{
    m_x -= cos(angle_x) * CAMERA_SPEED;
    m_y += sin(angle_x) * CAMERA_SPEED;
}

void Camera::shiftForward()
{
    m_x += sin(angle_x) * CAMERA_SPEED;
    m_y += cos(angle_x) * CAMERA_SPEED;
}

void Camera::shiftBack()
{
    m_x -= sin(angle_x) * CAMERA_SPEED;
    m_y -= cos(angle_x) * CAMERA_SPEED;
}

void Camera::shiftUp()
{
    m_z += CAMERA_SPEED;
}

void Camera::shiftDown()
{
    m_z -= CAMERA_SPEED;
}

void Camera::turnAroundZ(int value)
{
    angle_x += MOUSE_SENSIVITY * value;
}

void Camera::turnAroundX(int value)
{
    double nextZ = angle_z - MOUSE_SENSIVITY * value * ANGLE_Z_ROTATING_COEFF;
    if ((nextZ < 1) && (nextZ > -1))
    {
        angle_z = nextZ;
    }
}

void Camera::zoomIn()
{
    if (m_zoomStateIndex > 0)
    {
        --m_zoomStateIndex;
    }
}

void Camera::zoomOut()
{
    if (m_zoomStateIndex + 1 < ZOOM_STATES_COUNT)
    {
        ++m_zoomStateIndex;
    }
}

void Camera::refresh() const
{
    mat4d modelView;
    double scale = ZOOM_STATES[m_zoomStateIndex];
    vec3d center(m_x, m_y, scale * m_z);
    vec3d view_point;
    view_point.x = sin(angle_x) + center.x;
    view_point.y = cos(angle_x) + center.y;
    view_point.z = ANGLE_Z_MULTIPLIER * sin(angle_z) + center.z;
    modelView.lookAtRH(center.x, center.y, center.z,
                       view_point.x , view_point.y , view_point.z,
                       0, 0, 1);
    GLC::ClientState::loadModelView(modelView);
}

static inline
vec2d normalizeViewportCoord(const vec2d &pointXY)
{
    const double &x = pointXY.x;
    const double &y = pointXY.y;

    int viewport[4];
    GLC::ClientState::getViewport(viewport);

    vec2d vpSize(viewport[2], viewport[3]);
    vec2d vpCenter(viewport[0] + vpSize.x / 2, viewport[1] + vpSize.y / 2);

    vec2d ret;
    ret.x = 2 * (x - vpCenter.x) / vpSize.x;
    ret.y = -2 * (y - vpCenter.y) / vpSize.y;

    return ret;
}

static inline
vec3d unProjectPoint(const mat4d &inverse,
                     const vec2d &normXY,
                     const double &depth)
{
    vec4d pos = inverse * vec4d(normXY.x, normXY.y, depth, 1);
    return pos.project();
}

//! TODO: try to implement this using gluUnproject()
vec3d Camera::unProject(const vec2d &pointXY)
{
    vec2d normXY = normalizeViewportCoord(pointXY);
    mat4d modelView, projection;
    GLC::ClientState::getModelView(modelView);
    GLC::ClientState::getProjection(projection);
    mat4d inverse = (projection * modelView).getInverseMatrix();

    vec3d start = inverse.transform(vec3d(normXY, -1.0)); //unProjectPoint(inverse, normXY, -1);
    vec3d end = inverse.transform(vec3d(normXY, +1.0));
    vec3d dir = end - start;

    // time when ray Z coord becomes 0
    double time0 = -start.z / dir.z;

    return vec3d(start.x + time0 * dir.x,
                 start.y + time0 * dir.y,
                 0);
}

} // namespace Demo

#if 0
    GLint viewport[4];
    glGetIntegerv(GL_VIEWPORT, viewport);

    GLdouble modelview[16];
    glGetDoublev(GL_MODELVIEW_MATRIX, modelview);
    GLdouble projection[16];
    glGetDoublev(GL_PROJECTION_MATRIX, modelview);

    vec3d realPoint;
    realPoint.x = pointXY.x;
    realPoint.y = viewport[3] - 1 - pointXY.y;

    myDebug() << "real xy: " << realPoint.x << ' ' << realPoint.y << "\n";
    float localz = 0;
    glReadPixels(realPoint.x, realPoint.y, 1, 1,
                 GL_DEPTH_COMPONENT, GL_FLOAT, &localz);

    myDebug() << "Camera::unProject realPoint.z after reading: " << localz << "\n";
    realPoint.z = localz;

    vec3d ret;
    GLint status = gluUnProject(realPoint.x, realPoint.y, realPoint.z,
                                modelview, projection, viewport,
                                &ret.x, &ret.y, &ret.z);
    if (status == GLU_FALSE)
    {
        myDebug() << "gluUnProject() failed in Demo::Camera::unProject() proc\n";
    }
    if (glGetError() != GL_NO_ERROR)
        myDebug() << "ERROR OCCURED!!!!\n";

    return ret;
#endif

#if 0
    vec3d posnear, posfar;
    GLint status = gluUnProject(pointXY.x, pointXY.y, 0.01,
                 modelview, projection, viewport,
                 &posnear.x, &posnear.y, &posnear.z);
    if (status == GLU_FALSE)
    {
        myDebug() << "gluUnProject() failed in Demo::Camera::unProject() proc\n";
    }
    status = gluUnProject(pointXY.x, pointXY.y, 100.0,
                 modelview, projection, viewport,
                 &posfar.x, &posfar.y, &posfar.z);
    if (status == GLU_FALSE)
    {
        myDebug() << "gluUnProject() failed in Demo::Camera::unProject() proc\n";
    }
    if (glGetError() != GL_NO_ERROR)
        myDebug() << "ERROR OCCURED!!!!\n";

    vec3d mouseRay = posfar - posnear;
    vec3d normal(0, 0, 1);
    double time = dot(normal, -mouseRay) / dot(normal, mouseRay);

    return (posnear + time * mouseRay);
#endif
